Argomento |
---|
Scripting |
Livello di difficoltà |
Base |
Tempo di esecuzione |
Autori |
onekk Carlo |
Versione di FreeCAD |
0.19 |
Files di esempio |
Vedere anche |
Nessuno |
Per Scripting intendiamo la creazione di oggetti topologici mediante l'uso dell'interprete Python interno di FreeCAD. FreeCAD può essere usato come "ottimo" sostituto di OpenSCAD, principalmente perché possiede un vero interprete Python, questo vuol dire che ha un intero linguaggio di programmazione "a bordo", quasi tutto quello che si può realizzare con l'interfaccia grafica è possibile realizzarlo anche attraverso uno script Python.
Purtroppo le informazioni che riguardano lo scripting nella documentazione di FreeCAD, e anche in questo wiki sono disperse e mancano di una uniformità di "scrittura" e molte di esse sono spiegate in maniera troppo tecnica.
Il primo ostacolo ad un semplice approccio allo scripting deriva dal fatto che non esiste un modo per accedere direttamente all'editor Python interno a FreeCAD, con un comando di menù od un'icona nella barra degli strumenti, sapendo però che FreeCAD apre un file con estensione .py
nell'editor Python interno, il trucco più semplice è quello di creare usando il proprio editor di testo preferito, un file e poi aprirlo in FreeCAD con File → Apri.
Per fare le cose con un minimo di stile, il file deve essere scritto con un certo ordine. L'editor di FreeCAD possiede una buona "Evidenziazione di Sintassi" che manca a molti editor come Windows Notepad o altri editor di Linux di base, per cominciare basta scrivere queste poche righe:
"""filename.py
A short description of what the script does
"""
Salvatele con un nome significativo e con estensione .py
e caricate il file ottenuto in FreeCAD, con il comando File → Apri.
Un esempio minimale che contiene tutto quanto necessario per uno script è mostrato in questa porzione di codice, che potete usare come modello per quasi ogni vostro futuro script:
"""filename.py
First FreeCAD Script
"""
import FreeCAD
from FreeCAD import Placement, Rotation, Vector
import FreeCADGui
DOC_NAME = "Wiki_Example"
DOC = FreeCAD.newDocument(DOC_NAME)
FreeCAD.setActiveDocument(DOC.Name)
ROT0 = Rotation(0, 0, 0)
VEC0 = Vector(0, 0, 0)
# Helper function
def set_view():
"""Rearrange View."""
if not FreeCAD.GuiUp:
return
doc = FreeCADGui.ActiveDocument
if doc is None:
return
view = doc.ActiveView
if view is None:
return
# Check if the view is a 3D view:
if not hasattr(view, "getSceneGraph"):
return
view.viewAxometric()
view.fitAll()
Nel codice qui sopra sono presenti alcuni trucchi:
import FreeCAD
Questa linea serve per importare FreeCAD all'interno dell'interprete Python, può sembrare superfluo, ma non lo è.from FreeCAD import Placement, Rotation, Vector
Placement Rotation e Vector sono molto usati negli script in FreeCAD, importando questi due metodi in questo modo vi evita di scrivere FreeCAD.Vector
oppure FreeCAD.Placement
al posto di Vector
oppure Placement
, vi risparmiano quindi molto lavoro di battitura e rendono il codice più compatto.Cominciamo con un piccolo script che fa un piccolo lavoro, ma mostra la potenza di questo approccio.
# Script functions
def my_box(name, len, wid, hei):
"""Create a box."""
obj_b = DOC.addObject("Part::Box", name)
obj_b.Length = len
obj_b.Width = wid
obj_b.Height = hei
DOC.recompute()
return obj_b
# objects definition
obj = my_box("test_cube", 5, 5, 5)
set_view()
Scrivete sopra le righe di codice dopo # Script functions
e premete la freccia verde nella Barra degli strumenti Macro.
Accadranno alcune magie, si apre un nuovo documento chiamato "Wiki_example" e si visualizza un Cubo nella Vista 3D, che dovrebbe assomigliare all'immagine qui sotto.
Niente di eccezionale? Vero, ma da qualcosa dobbiamo pure incominciare, possiamo fare la stessa cosa con un Cilindro, aggiungete queste linee dopo la funzione my_box()
e prima della riga # objects definition
.
def my_cyl(name, ang, rad, hei):
"""Create a Cylinder."""
obj = DOC.addObject("Part::Cylinder", name)
obj.Angle = ang
obj.Radius = rad
obj.Height = hei
DOC.recompute()
return obj
Anche qui nulla di eccezionale. Notiamo alcune cose nella costruzione del codice:
App.
, presenti in molta documentazione che parla di scripting, è pienamente voluto, in futuro si potrà riusare il codice per accedere a FreeCAD come un modula da un interprete Python esterno, la cosa non è proprio facilissima da AppImage, ma con qualche accortezza è possibile. Di più facendo riferimento al motto di Python "esplicito è meglio che implicito", App.
non indica molto bene da dove arrivano i metodi che si usano.DOC = FreeCAD.activeDocument()
; activeDocument ovviamente non è una "costante", ma dal punto di vista semantico è il nostro "documento attivo", da qui l'uso della convenzione di Pyhton del nome "TUTTO MAIUSCOLO" per le "costanti", senza considerare che DOC
è molto pià corto che FreeCAD.activeDocument()
.Placement
, questo è voluto perché utilizzando geometrie semplici per creare geometrie più complesse, la gestione della proprietà Placement
è una cosa "delicata".Ora cosa dobbiamo fare con questi oggetti?
Introduciamo ora le operazioni booleane. Un esempio per cominciare, mettendo queste linee dopo my_cyl
, si crea una funzione che esegue una operazione di "Fusione" conosciuta anche come "Unione":
def fuse_obj(name, obj_0, obj_1):
"""Fuse two objects."""
obj = DOC.addObject("Part::Fuse", name)
obj.Base = obj_0
obj.Tool = obj_1
obj.Refine = True
DOC.recompute()
return obj
Anche qui nulla di eccezionale, notate comunque l'uniformità nel codice della funzione di scrittura; Questo approccio è molto più lineare di quello usato in molti altri Tutorial, aiuta molto ad incrementare la leggibilità del codice e anche quando si vuole fare copia e incolla.
Usiamo ora queste geometrie, cancellate le linee di codice dopo # objects definition
e inserite queste linee:
# objects definition
obj = my_box("test_cube", 5, 5, 5)
obj1 = my_cyl("test_cyl", 360, 2, 10)
fuse_obj("Fusion", obj, obj1)
set_view()
Lanciate lo script con la freccia verde e vedrete nella vista 3D, qualcosa che assomiglia all'immagine qui sotto:
Il concetto di Posizionamento è relativamente complesso, potete vedere Tutorial Aeroplano per una spiegazione più approfondita.
In genere abbiamo bisogno di posizionare una geometria rispetto ad un'altra geometria, cosa abbastanza frequente quando si creano geometrie complesse, il modo più comodo è quello di usare la proprietà Placement
della geometria.
FreeCAD offre un'ampia scelta di modi con cui specificare questa proprietà, molto dipende anche dalle conoscenze di fondo dell'utente, quella più facile da comprendere è quella spiegata nel Tutorial citato, usa una particolare definizione della porzione Rotation
all'interno della definizione di Placement
.
FreeCAD.Placement(Vector(0, 0, 0), FreeCAD.Rotation(10, 20, 30), Vector(0, 0, 0))
Comunque al di sopra di ogni ulteriore considerazione, una cosa è cruciale, il concetto di "punto di riferimento" della geometria. In altri termini, il punto dal quale l'oggetto viene costruito da parte di FreeCAD, riportiamo in questa tabella, copiata direttamente da Placement:
Oggetto | Punto di riferimento |
---|---|
Part.Box | vertice sinistro (minx), anteriore (miny), inferiore (minz). |
Parte.Sfera | centro della sfera |
Part.Cilindro | centro della faccia inferiore |
Part.Cono | centro della faccia inferiore (o apice se il raggio inferiore è 0) |
Part.Toro | centro del toro |
Funzioni derivate dagli schizzi | La caratteristica eredita la posizione dello schizzo sottostante. Gli schizzi iniziano sempre con Posizione = (0, 0, 0). Questa posizione corrisponde all'origine nello schizzo. |
Questa informazione va tenuta ben presente specie quando si applica una rotazione.
Alcuni esempi possono aiutare, eliminare la funzione my_box
e tutte le righe dopo la funzione my_cyl
e aggiungere il codice seguente dopo la funzione my_cyl
:
def my_sphere(name, rad):
"""Create a Sphere."""
obj = DOC.addObject("Part::Sphere", name)
obj.Radius = rad
DOC.recompute()
return obj
def my_box2(name, len, wid, hei, cent=False, off_z=0):
"""Create a box with an optional z offset."""
obj_b = DOC.addObject("Part::Box", name)
obj_b.Length = len
obj_b.Width = wid
obj_b.Height = hei
if cent is True:
pos = Vector(len * -0.5, wid * -0.5, off_z)
else:
pos = Vector(0, 0, off_z)
obj_b.Placement = Placement(pos, ROT0, VEC0)
DOC.recompute()
return obj_b
def mfuse_obj(name, objs):
"""Fuse multiple objects."""
obj = DOC.addObject("Part::MultiFuse", name)
obj.Shapes = objs
obj.Refine = True
DOC.recompute()
return obj
def airplane():
"""Create an airplane shaped solid."""
fuselage_length = 30
fuselage_diameter = 5
wing_span = fuselage_length * 1.75
wing_width = 7.5
wing_thickness = 1.5
tail_height = fuselage_diameter * 3.0
tail_position = fuselage_length * 0.70
tail_offset = tail_position - (wing_width * 0.5)
obj1 = my_cyl("main_body", 360, fuselage_diameter, fuselage_length)
obj2 = my_box2("wings", wing_span, wing_thickness, wing_width, True, tail_offset)
obj3 = my_sphere("nose", fuselage_diameter)
obj3.Placement = Placement(Vector(0, 0, fuselage_length), ROT0, VEC0)
obj4 = my_box2("tail", wing_thickness, tail_height, wing_width, False, 0)
obj4.Placement = Placement(Vector(0, tail_height * -1, 0), ROT0, VEC0)
objs = (obj1, obj2, obj3, obj4)
obj = mfuse_obj("airplane", objs)
obj.Placement = Placement(VEC0, Rotation(0, 0, -90), Vector(0, 0, tail_position))
DOC.recompute()
return obj
# objects definition
airplane()
set_view()
Illustriamo meglio alcuni punti del codice:
Shapes
dove abbiamo messo una tupla contenente gli oggetti da fondere, avremmo potuto se necessario passare una lista.Placement
per i vari componenti base della geometria e abbiamo definito la parte Rotation
della proprietà Placement
usando la scrittura Yaw-Pitch-Roll. Notate l'ultimo componente Vector(0,0, tail_position)
, questo definisce il "centro di rotazione" della geometria finale.![]() |
![]() |
![]() |
Potete facilmente notare che l'airplane ruota attorno al suo "baricentro" detto anche "centro di gravità", che ho fissato nel centro delle ali, una posizione abbastanza "naturale", potete comunque piazzarlo dove più vi aggrada o vi serve.
Il primo Vector(0,0,0)
è il vettore di Traslazione (o di posizionamento), che qui non abbiamo usato, però se sostituite la riga airplane()
con le linee seguenti:
obj_f = airplane()
print(obj_F.Placement)
Potete leggere nella finestra Report questo testo:
Placement [Pos=(0, -21, 21), Yaw-Pitch-Roll=(0, 0, -90)]
Cosa è successo?
FreeCAD ha "tradotto" il posizionamento passato con Vector(0, 0, 0), FreeCAD.Rotation(0, 0, -90), Vector(0, 0, tail_position)
, che specificava tre componenti Translazione, Rotazione e centro di rotazione nel suo valore "interno" che possiede solo due componenti, Translazione e Rotazione.
si può facilmente visualizzare il valore di tail_position
utilizzando un'istruzione print nella funzione airplane()
e vedere che è:
tail_position = 21.0
in parole parole, il centro di rotazione della geometria è posizionato a Vector(0, 0, 21)
, ma non è mostrato attraverso l'interfaccia grafica nella vista Dati, può essere specificato come valore nella proprietà Placement
, ma non può essere facilmente recuperato.
Questo è il significato dell'aggettivo "delicato" che ho usato precedentemente nel testo per definire la proprietà Placement
.
Questo è l'esempio di codice completo con una docstring di script decente che segue la convenzione sulle docstring di Google:
"""Sample code.
Filename:
airplane.py
Author:
Dormeletti Carlo (onekk)
Version:
1.0
License:
Creative Commons Attribution 3.0
Summary:
This is sample code written for a FreeCAD Wiki page.
It creates an airplane shaped solid using standard "Part WB" shapes.
"""
import FreeCAD
from FreeCAD import Placement, Rotation, Vector
import FreeCADGui
DOC_NAME = "Wiki_Example"
DOC = FreeCAD.newDocument(DOC_NAME)
FreeCAD.setActiveDocument(DOC.Name)
ROT0 = Rotation(0, 0, 0)
VEC0 = Vector(0, 0, 0)
# Helper function
def set_view():
"""Rearrange View."""
if not FreeCAD.GuiUp:
return
doc = FreeCADGui.ActiveDocument
if doc is None:
return
view = doc.ActiveView
if view is None:
return
# Check if the view is a 3D view:
if not hasattr(view, "getSceneGraph"):
return
view.viewAxometric()
view.fitAll()
# Script functions
def my_cyl(name, ang, rad, hei):
"""Create a Cylinder."""
obj = DOC.addObject("Part::Cylinder", name)
obj.Angle = ang
obj.Radius = rad
obj.Height = hei
DOC.recompute()
return obj
def my_sphere(name, rad):
"""Create a Sphere."""
obj = DOC.addObject("Part::Sphere", name)
obj.Radius = rad
DOC.recompute()
return obj
def my_box2(name, len, wid, hei, cent=False, off_z=0):
"""Create a box with an optional z offset."""
obj_b = DOC.addObject("Part::Box", name)
obj_b.Length = len
obj_b.Width = wid
obj_b.Height = hei
if cent is True:
pos = Vector(len * -0.5, wid * -0.5, off_z)
else:
pos = Vector(0, 0, off_z)
obj_b.Placement = Placement(pos, ROT0, VEC0)
DOC.recompute()
return obj_b
def mfuse_obj(name, objs):
"""Fuse multiple objects."""
obj = DOC.addObject("Part::MultiFuse", name)
obj.Shapes = objs
obj.Refine = True
DOC.recompute()
return obj
def airplane():
"""Create an airplane shaped solid."""
fuselage_length = 30
fuselage_diameter = 5
wing_span = fuselage_length * 1.75
wing_width = 7.5
wing_thickness = 1.5
tail_height = fuselage_diameter * 3.0
tail_position = fuselage_length * 0.70
tail_offset = tail_position - (wing_width * 0.5)
obj1 = my_cyl("main_body", 360, fuselage_diameter, fuselage_length)
obj2 = my_box2("wings", wing_span, wing_thickness, wing_width, True, tail_offset)
obj3 = my_sphere("nose", fuselage_diameter)
obj3.Placement = Placement(Vector(0, 0, fuselage_length), ROT0, VEC0)
obj4 = my_box2("tail", wing_thickness, tail_height, wing_width, False, 0)
obj4.Placement = Placement(Vector(0, tail_height * -1, 0), ROT0, VEC0)
objs = (obj1, obj2, obj3, obj4)
obj = mfuse_obj("airplane", objs)
obj.Placement = Placement(VEC0, Rotation(0, 0, -90), Vector(0, 0, tail_position))
DOC.recompute()
return obj
# objects definition
airplane()
set_view()